using System;
using System.Collections.Generic;
using System.Linq;
using Arroguella.Entities;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Diagnostics;
using Arroguella.Utilities;

namespace Arroguella.Tiles
{
    /// <summary>
    /// This engine is responsible for managing all the tiles.
    /// </summary>
    public class TileEngine : GameComponent
    {

        #region Constructor

        /// <summary>
        /// Default constructor
        /// </summary>
        public TileEngine(Game game, Rectangle tileRegion, Guid seed)
            : base(game)
        {
            arroguella = game as Arroguella;
            Debug.Assert(arroguella != null, "Arroguella != null");
            Font = arroguella.Font;
            GameMapArea = tileRegion;
            TileMap = new Dictionary<Vector2, Tile>();
            ColorTextureMap = new Dictionary<Color, Texture2D>();
            Seed = seed;
        }

        #endregion

        #region Events
        #endregion

        #region Methods

        /// <summary>
        /// Allows the game component to perform any initialization it needs to before starting
        /// to run.  This is where it can query for any required services and load content.
        /// </summary>
        public override void Initialize()
        {
            GenerateMap(0);

            base.Initialize();
        }

        /// <summary>
        /// Generates the map based on level.
        /// </summary>
        public void GenerateMap(int level)
        {
            IsGridEnabled = true;

            InitializeTiles();

            DungeonMapGenerator dungeonMapGenerator = new DungeonMapGenerator(TileMap, Seed);
            dungeonMapGenerator.MakeMap(level);

            InitializeColorTextureMap();

            // Some optimization - let's disqualify unnecessary tiles from needing to draw
            foreach (Vector2 tileCoords in TileMap.Keys)
            {
                if (IsTileSurroundedByWall(tileCoords))
                {
                    TileMap[tileCoords].NeverDraw = true;
                }

                if (IsTileUnnecessaryEdge(tileCoords))
                {
                    TileMap[tileCoords].NeverDraw = true;
                }
            }
        }

        /// <summary>
        /// Allows the game component to update itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        public override void Update(GameTime gameTime)
        {
            // TODO - This isn't quite in sync with DRAW. Creatures may jump several tiles between DRAWs.
            bool needToUpdateCreatures = creatureUpdateTimeElapsed >= TimeSpan.FromMilliseconds(1000);
            if (needToUpdateCreatures)
            {
                creatureUpdateTimeElapsed = new TimeSpan(0, 0, 0);
            }
            else
            {
                creatureUpdateTimeElapsed += gameTime.ElapsedGameTime;
            }

            // Update Creature AI
            if (needToUpdateCreatures)
            {
                foreach (Tile tile in TileMap.Values.Where(tile => !tile.NeverDraw).Where(tile => tile.Objects.Count > 0))
                {
                    UpdateCreatureEntities(gameTime, tile);
                } 
            }

            base.Update(gameTime);
        }

        /// <summary>
        /// Updates all CreatureEntities (via their AiComponent) on a particular tile
        /// </summary>
        private void UpdateCreatureEntities(GameTime gameTime, Tile tile)
        {
            Dictionary<CreatureEntity, Tile> intendedMovements = new Dictionary<CreatureEntity, Tile>();

            foreach (CreatureEntity creature in tile.Objects.OfType<CreatureEntity>())
            {
                creature.Update(tile);

                // Handle the AI's decisions it made during the Update.
                SimpleWanderingAiComponent ai = creature.Ai as SimpleWanderingAiComponent;
                if (ai != null)
                {
                    Tile intendedMoveTile = TileMap[ai.IntendedMovementCoordinates];
                    if (intendedMoveTile != null && !intendedMoveTile.IsBlocking)
                    {
                        // Queue this up so we aren't modifying a collection while iterating over it
                        intendedMovements.Add(creature, intendedMoveTile);
                    }
                }
            }

            // Perform the movements
            foreach (KeyValuePair<CreatureEntity, Tile> movement in intendedMovements)
            {
                MoveCreature(movement.Key, tile, movement.Value); 
            }
        }

        /// <summary>
        /// Takes a creature from a source tile, and puts it in the destination tile.
        /// </summary>
        private void MoveCreature(CreatureEntity creature, Tile source, Tile destination)
        {
            source.Objects.Remove(creature);
            destination.Objects.Add(creature);
        }

        public void Draw(SpriteBatch spriteBatch)
        {
            foreach (var t in TileMap)
            {
                // Draw it
                if (t.Value.IsExplored && !t.Value.NeverDraw)
                {
                    // Get the location, in pixels.
                    int x0 = GameMapArea.Left;
                    int y0 = GameMapArea.Top;
                    Vector2 location = new Vector2(x0 + t.Key.X * Tile.Size.X, y0 + t.Key.Y * Tile.Size.Y);
                
                    DrawTile(t.Value, location, spriteBatch);
                }
            }
        }

        private void DrawTile(Tile t, Vector2 location, SpriteBatch spriteBatch)
        {
            // Get the tile region, in pixels.
            Rectangle tileRect = new Rectangle((int)location.X, (int)location.Y, (int)Tile.Size.X, (int)Tile.Size.Y);

            // Draw it
            DrawRectangle(tileRect, t.Color, spriteBatch);

            // Draw the tile's applicable symbol
            Utils.DrawString(spriteBatch, Font, t.Symbol, t.SymbolColor, tileRect);

            // Draw a highlight
            if (t.IsHighlight)
            {
                Utils.DrawRectangleBorder(tileRect, 1, Color.DodgerBlue, spriteBatch, GraphicsDevice);
            }

            // Draw the grid.
            if (IsGridEnabled && !t.IsBlocking && !Arroguella.OldSchoolModeEnabled)
            {
                Utils.DrawRectangleBorder(GRID_TEXTURE, tileRect, 1, GRID_COLOR, spriteBatch);
            }
        }

        /// <summary>
        /// Utility method for drawing a rectangle given coordinates and a color.
        /// The TileEngine uses a specialized DrawRectangle to make use of the ColorTextureMap.
        /// </summary>
        private void DrawRectangle(Rectangle rect, Color color, SpriteBatch spriteBatch)
        {
            if (ColorTextureMap.ContainsKey(color))
            {
                spriteBatch.Draw(ColorTextureMap[color], rect, color);
            }
            else
            {
                var texture = new Texture2D(GraphicsDevice, 1, 1);
                texture.SetData(new[] { color });
                ColorTextureMap.Add(color, texture);

                spriteBatch.Draw(texture, rect, color);
            }
        }

        /// <summary>
        /// Lights the tiles in a circle at a center.
        /// </summary>
        /// <param name="center">The tile coordinates where the light is centered.</param>
        /// <param name="radius">The radius, in tiles.</param>
        /// <param name="intensity">A value from -1 to 1 on how intense the light is.</param>
        public void LightCircle(Vector2 center, int radius, float intensity)
        {
            // First, reset each tile
            foreach (Vector2 coords in TileMap.Keys)
            {
                UnlightTile(coords);
            }

            int x0 = (int)center.X;
            int y0 = (int)center.Y;

            for (int y = -radius; y <= radius; y++)
            {
                for (int x = -radius; x <= radius; x++)
                {
                    if (x * x + y * y <= radius * radius + radius * 0.8f)
                    {
                        LightTile(x0 + x, y0 + y, intensity);
                    }
                }                    
            }                
        }

        /// <summary>
        /// Lights the tiles in a circle at a center.
        /// Allows for greater control over the R, G, and B light intensities.
        /// </summary>
        public void LightCircle(Vector2 center, int radius, float redIntensity, float greenIntensity, float blueIntensity)
        {
            // First, reset each tile
            foreach (Vector2 coords in TileMap.Keys)
            {
                UnlightTile(coords);
            }

            int x0 = (int)center.X;
            int y0 = (int)center.Y;

            for (int y = -radius; y <= radius; y++)
            {
                for (int x = -radius; x <= radius; x++)
                {
                    if (x * x + y * y <= radius * radius + radius * 0.8f)
                    {
                        LightTile(x0 + x, y0 + y, redIntensity, greenIntensity, blueIntensity);
                    }
                }
            }
        }

        /// <summary>
        /// Lights a tile.
        /// This tile is assumed to be in the player's FOV now.
        /// </summary>
        private void LightTile(int x, int y, float intensity)
        {
            Vector2 tileCoords = new Vector2(x, y);
            if (!TileMap.ContainsKey(tileCoords))
            {
                return;
            }

            Tile t = TileMap[tileCoords];
            t.LightIntensity = intensity;
            t.IsExplored = true;
        }

        /// <summary>
        /// Lights a tile, with fine control over R, G, B light intensities.
        /// This tile is assumed to be in the player's FOV now.
        /// </summary>
        private void LightTile(int x, int y, float redIntensity, float greenIntensity, float blueIntensity)
        {
            Vector2 tileCoords = new Vector2(x, y);
            if (!TileMap.ContainsKey(tileCoords))
            {
                return;
            }

            Tile t = TileMap[tileCoords];
            t.LightIntensityRed = redIntensity;
            t.LightIntensityGreen = greenIntensity;
            t.LightIntensityBlue = blueIntensity;
            t.IsExplored = true;
        }

        /// <summary>
        /// Unlights a tile.
        /// The tile is assumed to no longer be in the player's FOV.
        /// </summary>
        private void UnlightTile(Vector2 tileCoords)
        {
            Tile t = TileMap[tileCoords];
            t.LightIntensity = 0f;
            t.LightIntensityRed = 0f;
            t.LightIntensityGreen = 0f;
            t.LightIntensityBlue = 0f;
        }

        /// <summary>
        /// Populates the TileMap with empty tiles.
        /// </summary>
        private void InitializeTiles()
        {
            int width = GameMapArea.Width;
            int height = GameMapArea.Height;

            NumberOfTileColumns = (int)(width / Tile.Size.X);
            NumberOfTileRows = (int)(height / Tile.Size.Y);

            for (int row = 0; row < NumberOfTileRows; row++)
            {
                for (int col = 0; col < NumberOfTileColumns; col++)
                {
                    Vector2 tileCoords = new Vector2(col, row);
                    Tile t = new Tile(arroguella, tileCoords);
                    TileMap.Add(tileCoords, t);                    
                }
            }            

            // Initialize grid
            GRID_TEXTURE = new Texture2D(GraphicsDevice, 1, 1);
            GRID_TEXTURE.SetData(new[] { GRID_COLOR });
        }

        /// <summary>
        /// Initializes a color texuture map for better performance.
        /// </summary>
        private void InitializeColorTextureMap()
        {
            foreach (Tile t in TileMap.Values)
            {
                // Add the color to the texture map if we haven't already
                if (!ColorTextureMap.ContainsKey(t.Color))
                {
                    var texture = new Texture2D(GraphicsDevice, 1, 1);
                    texture.SetData(new[] { t.Color });
                    ColorTextureMap.Add(t.Color, texture);
                }
            }

            Console.WriteLine("Initialized " + TileMap.Count + " tiles, with " + ColorTextureMap.Count + " different colors.");
        }

        /// <summary>
        /// Converts pixel coordinates into tile coordinates.
        /// Assumes that (0, 0) in pixel coordinates is at the top left of the game map.
        /// </summary>
        /// <param name="px">x-coordinate, in pixels</param>
        /// <param name="py">y-coordinate, in pixels</param>
        /// <returns>A Vector2 representing the tile coordinates, with -1 standing in for any invalid pixel value.</returns>
        public Vector2 ConvertPixelCoordinatesToTileCoordinates(int px, int py)
        {
            int width = GameMapArea.Width;
            int height = GameMapArea.Height;

            int x, y;

            if (px >= 0 && px <= width)
            {
                x = (int)(px / Tile.Size.X);
            }
            else
            {
                x = -1;
            }

            if (py >= 0 && py <= height)
            {
                y = (int)(py / Tile.Size.Y);
            }
            else
            {
                y = -1;
            }

            return new Vector2(x, y);
        }

        /// <summary>
        /// Gets whether or not a tile is completely surrounded by walls
        /// or other blocking structures.
        /// </summary>
        private bool IsTileSurroundedByWall(Vector2 tileCoords)
        {
            int x = (int)tileCoords.X;
            int y = (int)tileCoords.Y;

            if (x > 0
                && x < NumberOfTileColumns - 1
                && y > 0
                && y < NumberOfTileRows - 1)
            {
                Vector2 north = new Vector2(x, y - 1);
                Vector2 northEast = new Vector2(x + 1, y - 1);
                Vector2 east = new Vector2(x + 1, y);
                Vector2 southEast = new Vector2(x + 1, y + 1);
                Vector2 south = new Vector2(x, y + 1);
                Vector2 southWest = new Vector2(x - 1, y + 1);
                Vector2 west = new Vector2(x - 1, y);
                Vector2 northWest = new Vector2(x - 1, y - 1);

                return TileMap[north].IsBlocking
                    && TileMap[northEast].IsBlocking
                    && TileMap[east].IsBlocking
                    && TileMap[southEast].IsBlocking
                    && TileMap[south].IsBlocking
                    && TileMap[southWest].IsBlocking
                    && TileMap[west].IsBlocking
                    && TileMap[northWest].IsBlocking;
            }

            return false;
        }

        /// <summary>
        /// Checks the tiles around the edge of the map to see if it borders any floor.
        /// If it borders ANY floor, it is necessary and must be drawn.
        /// Otherwise, it is unnecessary to draw.
        /// </summary>
        private bool IsTileUnnecessaryEdge(Vector2 tileCoords)
        {
            int x = (int)tileCoords.X;
            int y = (int)tileCoords.Y;

            bool isBlocked;

            // Left edge, can't have NW, W, SW tiles
            if (x == 0 && (y > 0 && y < NumberOfTileRows - 1))
            {
                Vector2 north = new Vector2(x, y - 1);
                Vector2 northEast = new Vector2(x + 1, y - 1);
                Vector2 east = new Vector2(x + 1, y);
                Vector2 southEast = new Vector2(x + 1, y + 1);
                Vector2 south = new Vector2(x, y + 1);

                isBlocked = TileMap[north].IsBlocking
                && TileMap[northEast].IsBlocking
                && TileMap[east].IsBlocking
                && TileMap[southEast].IsBlocking
                && TileMap[south].IsBlocking;
            }
            // Top edge, can't have NW, N, NE tiles
            else if ((x > 0 && x < NumberOfTileColumns - 1) && y == 0)
            {
                Vector2 east = new Vector2(x + 1, y);
                Vector2 southEast = new Vector2(x + 1, y + 1);
                Vector2 south = new Vector2(x, y + 1);
                Vector2 southWest = new Vector2(x - 1, y + 1);
                Vector2 west = new Vector2(x - 1, y);

                isBlocked = TileMap[east].IsBlocking
                && TileMap[southEast].IsBlocking
                && TileMap[south].IsBlocking
                && TileMap[southWest].IsBlocking
                && TileMap[west].IsBlocking;
            }
            // Right edge, can't have NE, E, SE tiles
            else if (x == NumberOfTileColumns - 1 && (y > 0 && y < NumberOfTileRows - 1))
            {
                Vector2 north = new Vector2(x, y - 1);
                Vector2 south = new Vector2(x, y + 1);
                Vector2 southWest = new Vector2(x - 1, y + 1);
                Vector2 west = new Vector2(x - 1, y);
                Vector2 northWest = new Vector2(x - 1, y - 1);

                isBlocked = TileMap[north].IsBlocking
                && TileMap[south].IsBlocking
                && TileMap[southWest].IsBlocking
                && TileMap[west].IsBlocking
                && TileMap[northWest].IsBlocking;
            }
            // Bottom edge, can't have SW, S, SE tiles
            else if ((x > 0 && x < NumberOfTileColumns - 1) && y == NumberOfTileRows - 1)
            {
                Vector2 north = new Vector2(x, y - 1);
                Vector2 northEast = new Vector2(x + 1, y - 1);
                Vector2 east = new Vector2(x + 1, y);
                Vector2 west = new Vector2(x - 1, y);
                Vector2 northWest = new Vector2(x - 1, y - 1);

                isBlocked = TileMap[north].IsBlocking
                && TileMap[northEast].IsBlocking
                && TileMap[east].IsBlocking
                && TileMap[west].IsBlocking
                && TileMap[northWest].IsBlocking;
            }
            // Top Left Corner, can only have E, SE, S tiles
            else if (x == 0 && y == 0)
            {
                Vector2 east = new Vector2(x + 1, y);
                Vector2 southEast = new Vector2(x + 1, y + 1);
                Vector2 south = new Vector2(x, y + 1);

                isBlocked = TileMap[east].IsBlocking
                && TileMap[southEast].IsBlocking
                && TileMap[south].IsBlocking;
            }
            // Top Right Corner, can only have S, SW, W tiles
            else if (x == NumberOfTileColumns - 1 && y == 0)
            {
                Vector2 south = new Vector2(x, y + 1);
                Vector2 southWest = new Vector2(x - 1, y + 1);
                Vector2 west = new Vector2(x - 1, y);

                isBlocked = TileMap[south].IsBlocking
                && TileMap[southWest].IsBlocking
                && TileMap[west].IsBlocking;
            }
            // Bottom Right Corner, can only have N, NW, W tiles
            else if (x == NumberOfTileColumns - 1 && y == NumberOfTileRows - 1)
            {
                Vector2 north = new Vector2(x, y - 1);
                Vector2 west = new Vector2(x - 1, y);
                Vector2 northWest = new Vector2(x - 1, y - 1);

                isBlocked = TileMap[north].IsBlocking
                && TileMap[west].IsBlocking
                && TileMap[northWest].IsBlocking;
            }
            // Bottom Left Corner, can only have N, NE, E tiles
            else if (x == 0 && y == NumberOfTileRows - 1)
            {
                Vector2 north = new Vector2(x, y - 1);
                Vector2 northEast = new Vector2(x + 1, y - 1);
                Vector2 east = new Vector2(x + 1, y);

                isBlocked = TileMap[north].IsBlocking
                && TileMap[northEast].IsBlocking
                && TileMap[east].IsBlocking; 
            }
            else
            {
                return false;
            }

            return isBlocked;
        }

        #endregion

        #region Properties

        private readonly Arroguella arroguella;

        private GraphicsDevice GraphicsDevice { get { return arroguella.GraphicsDevice; } }

        /// <summary>
        /// The area in which the tiles may be drawn.
        /// </summary>
        private Rectangle GameMapArea { get; set; }

        /// <summary>
        /// A collection of the tiles currently managed by the TileEngine.
        /// They are indexed by their coordinates in the GameMap,
        /// with the origin tile (0, 0) in the top left corner.
        /// </summary>
        public Dictionary<Vector2, Tile> TileMap { get; private set; }

        /// <summary>
        /// Provides a mapping of color to texture for easy processing during drawing.
        /// This way we don't need to create a new texture on the fly,
        /// and we don't have multiple textures per color.
        /// </summary>
        private Dictionary<Color, Texture2D> ColorTextureMap { get; set; }

        private bool IsGridEnabled { get; set; }

        /// <summary>
        /// The color of the grid.
        /// </summary>
        private readonly Color GRID_COLOR = Color.FromNonPremultiplied(Color.Gainsboro.R, Color.Gainsboro.G, Color.Gainsboro.B, 50);

        internal static List<Color> WallColorPallete = new List<Color>{
            Color.DimGray
        };

        internal static List<Color> FloorColorPallete = new List<Color>{
            Color.FromNonPremultiplied(73, 71, 71, 255),
            Color.FromNonPremultiplied(71, 73, 71, 255),
            Color.FromNonPremultiplied(71, 71, 73, 255),
            Color.FromNonPremultiplied(75, 75, 75, 255),
            Color.FromNonPremultiplied(80, 80, 80, 255),
        };

        /// <summary>
        /// The texture used to draw the grid.
        /// </summary>
        private Texture2D GRID_TEXTURE;

        public int NumberOfTileRows { get; private set; }

        public int NumberOfTileColumns { get; private set; }

        public Vector2 PlayerStartPosition
        {
            get
            {
                foreach (Tile t in TileMap.Values.Where(t => t.PlayerStart))
                {
                    return t.Coords;
                }

                Debug.Assert(false, "No viable start position found for player.");

                return new Vector2((int)(NumberOfTileColumns / 2), (int)(NumberOfTileRows / 2));
            }
        }

        private SpriteFont Font { get; set; }

        private Guid Seed { get; set; }

        private TimeSpan creatureUpdateTimeElapsed = new TimeSpan(0, 0, 0);

        #endregion


    }
}
